BIOMECÁNICA - Laboratorio de análisis de la marcha¶
Yamil Alexander Ballestas Casallas[1], Caleb David Romero Mercado[2]
[1] Ingeniería Biomédica y electrónica, Universidad Tecnológica de Bolívar, ballestasy@utb.edu.co
[2] Tutor, Magister en Ingenieria Electrónica, Universidad Tecnológica de Bolívar, cromero@utb.edu.co
¿Cómo citar?: Y. Ballestas-Casallas, Biomecánica: Laboratorio de análisis de la marcha, Universidad Tecnologica de Bolívar, 2025.
RESUMEN¶
Se realizó un análisis biomecánico de la marcha a partir de datos de una base de datos con posiciones (x,y) de marcadores en la cadera, rodilla y tobillo. Se calcularon los vectores de los segmentos de la pierna y los ángulos articulares mediante diferencias angulares: cadera-costillas con rodilla-cadera, que permitió evaluar la movilidad de la cadera; y rodilla-cadera con tobillo-rodilla, la cual permitió estimar la flexión y extensión de la rodilla. Estos ángulos reflejan la coordinación del movimiento durante la marcha, por lo que son fundamentales en biomecánica.
Se estimó la velocidad y aceleración angular a partir del desplazamiento angular usando np.diff() en Python. Se emplearon pandas, numpy y matplotlib para el análisis y visualización, aplicando un filtro de suavizado (rolling(window=6).mean()) para reducir ruido. Se graficó la variación angular en el tiempo, señalando las fases de la marcha y los valores máximos y mínimos, y se extrajo el tiempo de duración de cada paso de la marcha. Este estudio permite evaluar la eficiencia del movimiento, con aplicaciones en clínica y rehabilitación.
I. FUNDAMENTOS¶
El análisis de la marcha es fundamental en biomecánica para evaluar alteraciones en la locomoción humana. La marcha normal consta de dos fases principales: fase de apoyo (stance) y fase de oscilación (swing), las cuales se subdividen en seis eventos clave.[1]
La fase de apoyo inicia con el Initial Double Limb Support (IDLS), cuando ambos pies están en contacto con el suelo y ocurre el heel strike. Luego, en la Single Limb Stance (SLS), el peso se transfiere a un solo pie mientras el otro inicia su oscilación, culminando en el Second Double Limb Support (SDLS), donde el pie contralateral hace contacto con el suelo, asegurando estabilidad antes del despegue (toe-off) [1][2]
La fase de oscilación comienza con el Initial Swing (IS), en el que el pie se despega del suelo con una rápida flexión de rodilla. En el Mid Swing (MS), la tibia se verticaliza y el pie alcanza su máxima altura respecto al suelo (foot clearance), facilitando un desplazamiento sin obstáculos. Finalmente, en el Terminal Swing (TS), la pierna en oscilación se prepara para el siguiente heel strike, completando el ciclo de marcha.[1][2][3]
El patrón de marcha varía según factores como el tipo de superficie, calzado y condición física del individuo. Alteraciones en estas fases pueden indicar patologías neuromusculares u ortopédicas, lo que resalta la importancia del análisis cinemático y cinético en su evaluación.[3]
II. DESARROLLO¶
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
# Extraer las columnas de interés y manejar valores no numéricos
def extract_valid_column(df, column_index):
column_data = pd.to_numeric(df.iloc[2:, column_index], errors='coerce')
return column_data.values
# Lectura de datos desde Excel
df = pd.read_excel('gait.xls')
t = df.iloc[2:, 1]
xb = extract_valid_column(df, 3) #base rib cage
yb = extract_valid_column(df, 4)
xh = extract_valid_column(df, 6) # hip
yh = extract_valid_column(df, 7)
xk = extract_valid_column(df, 9) # knee
yk = extract_valid_column(df, 10)
xa = extract_valid_column(df, 15) # ankle
ya = extract_valid_column(df, 16)
1. Ángulo de la cadera¶
Corresponde al ángulo entre los vectores cadera-costillas y rodilla-cadera
# ÁNGULO DE LA CADERA
hb = np.column_stack([xb - xh, yb - yh]) # vector 1
kh = np.column_stack([xh - xk, yh - yk]) # vector 2
# Ángulos de los vectores
theta_hb = np.arctan2(hb[:, 1], hb[:, 0])
theta_kh = np.arctan2(kh[:, 1], kh[:, 0])
# Ángulo de la cadera
hip = theta_hb - theta_kh # angle of the hip
hip_deg = np.degrees(hip) # angle in degrees
plt.figure(figsize=[20,8])
# Sin filtro
plt.subplot(131)
plt.plot(t, hip_deg, color = 'blue')
plt.title('Ángulo de la cadera')
plt.xlabel('tiempo (s)')
plt.ylabel('Ángulo θ')
# Con filtro
hip_deg_f = pd.Series(hip_deg).rolling(window=6).mean()
plt.subplot(132)
plt.plot(t, hip_deg_f, color = 'red')
plt.title('Ángulo de la cadera')
plt.xlabel('tiempo (s)')
plt.ylabel('Ángulo θ')
plt.subplot(133)
plt.plot(t, hip_deg, color = 'blue')
plt.plot(t, hip_deg_f, color = 'red')
plt.title('Ángulo de la cadera')
plt.xlabel('tiempo (s)')
plt.ylabel('Ángulo θ')
Text(0, 0.5, 'Ángulo θ')
2. Velocidad y aceleración angular de la cadera¶
# A partir de la gráfica de angulos filtrada, hallamos la velocidad angular de la cadera
# Ya que hemos eliminado gran parte del ruido
velocidad_hip = np.diff(hip_deg_f)/np.diff(t)
velocidad_hip_f = pd.Series(velocidad_hip).rolling(window=6).mean()
# A partir de la filtrada, hallamos la velocidad angular de la cadera
# Ya que hemos eliminado gran parte del ruido
aceleracion_hip = np.diff(velocidad_hip_f)
aceleracion_hip_f = pd.Series(aceleracion_hip).rolling(window=6).mean()
plt.figure(figsize=[20,8])
plt.subplot(131)
plt.plot(t[1:], velocidad_hip_f, color = 'blue')
plt.title('Velocidad de la cadera')
plt.xlabel('tiempo (s)')
plt.ylabel('Velocidad (°/s)')
plt.subplot(132)
plt.plot(t[2:], aceleracion_hip_f, color = 'red')
plt.title('Aceleración de la cadera')
plt.xlabel('tiempo (s)')
plt.ylabel('Aceleración (°/s^2)')
plt.subplot(133)
plt.plot(t[1:], velocidad_hip_f, color = 'blue')
plt.plot(t[2:], aceleracion_hip_f, color = 'red')
plt.title('Velocidad y aceleración de la cadera')
plt.xlabel('tiempo (s)')
plt.ylabel('Velocidad(°/s)/Aceleración (°/s^2)')
Text(0, 0.5, 'Velocidad(°/s)/Aceleración (°/s^2)')
3. Ángulo de la rodilla¶
Corresponde al ángulo entre los vectores rodilla-cadera y tobillo-rodilla
# ÁNGULO DEl TOBILLO-RODILLA
ak = np.column_stack([xk - xa, yk - ya]) # vector 3
# Ángulos de los vectores
theta_ak = np.arctan2(ak[:, 1], ak[:, 0])
theta_kh = np.arctan2(kh[:, 1], kh[:, 0])
# Ángulo de la rodilla
knee = theta_kh - theta_ak # angle of the knee
knee_deg = np.degrees(knee) # angle in degrees
plt.figure(figsize=[20,8])
# Sin filtro
plt.subplot(131)
plt.plot(t, knee_deg, color='blue')
plt.title('Ángulo de la rodilla')
plt.xlabel('tiempo (s)')
plt.ylabel('Ángulo θ')
# Con filtro
plt.subplot(132)
knee_deg_f = pd.Series(knee_deg).rolling(window=6).mean()
plt.plot(t, knee_deg_f, color='red')
plt.title('Ángulo de la rodilla')
plt.xlabel('tiempo (s)')
plt.ylabel('Ángulo θ')
plt.subplot(133)
plt.plot(t, knee_deg, color='blue')
plt.plot(t, knee_deg_f, color='red')
plt.title('Ángulo de la rodilla')
plt.xlabel('tiempo (s)')
plt.ylabel('Ángulo θ')
Text(0, 0.5, 'Ángulo θ')
4. Velocidad y aceleración angular de la rodilla¶
velocidad_knee = np.diff(knee_deg_f)/np.diff(t)
velocidad_knee_f = pd.Series(velocidad_knee).rolling(window=6).mean()
aceleracion_knee = np.diff(velocidad_knee_f)/ np.diff(t[1:])
aceleracion_knee_f = pd.Series(aceleracion_knee).rolling(window=6).mean()
plt.figure(figsize=[20,8])
plt.subplot(131)
plt.plot(t[1:], velocidad_knee_f, color = 'blue')
plt.title('Velocidad de la rodilla')
plt.xlabel('tiempo (s)')
plt.ylabel('Velocidad (°/s)')
plt.subplot(132)
plt.plot(t[2:], aceleracion_knee_f, color = 'red')
plt.title('Aceleración de la rodilla')
plt.xlabel('tiempo (s)')
plt.ylabel('Aceleración (°/s^2)')
plt.subplot(133)
plt.plot(t[1:], velocidad_knee_f, color = 'blue')
plt.plot(t[2:], aceleracion_knee_f, color = 'red')
plt.title('Velocidad y aceleración de la rodilla')
plt.xlabel('tiempo (s)')
plt.ylabel('Velocidad(°/s)/Aceleración (°/s^2)')
Text(0, 0.5, 'Velocidad(°/s)/Aceleración (°/s^2)')
III. ANÁLISIS¶
1. Ángulo de la rodilla en las fases de la marcha¶
# print (knee_deg_f.iloc[5])
plt.figure(figsize=[20,8])
knee_deg_f = np.array(knee_deg_f, dtype=float)
t = np.array(t , dtype=float)
indices_min = np.abs(knee_deg_f-(-0.3312591774437119)).argsort()[:4]
indice_max = np.abs(knee_deg_f-(max(knee_deg_f[5:]))).argsort()[:4]
# print(knee_deg_f[indices_min[0]])
# Graficar
plt.plot(t, knee_deg_f, 'black')
plt.axvline (x=t[indices_min[0]], color='b', linestyle='--', linewidth=3)
plt.text(1.4, 30, "Fin de marcha", color='b', fontsize=12, ha='center', rotation= 90, weight = 'bold')
plt.axvline (x=t[indices_min[3]], color='b', linestyle='--', linewidth=3)
plt.text(0.41, 30, "Inicio de marcha", color='b', fontsize=12, ha='center', rotation= 90, weight = 'bold')
# ANOTACIÓN DE MÁXIMA FLEXION
plt.annotate(f"Flexion máxima\n ({t[indice_max[2]]:.2f}, {knee_deg_f[indice_max[2]]:.2f})",
xy=(t[indice_max[2]], knee_deg_f[indice_max[2]]),
xytext=(0.8, 57),
color= 'r',
fontsize = 11,
weight = 'bold',
arrowprops=dict( facecolor="r", # Color de la flecha
edgecolor="r", # Contorno de la flecha
arrowstyle="->", # Estilo de flecha
linewidth=3 # Grosor de la flecha
))
# ANOTACIÓN DE MÁXIMA FLEXION
plt.annotate(f"Extensión máxima\n ({t[indices_min[3]]:.2f}, {knee_deg_f[indices_min[3]]:.2f})",
xy=(t[indices_min[3]], knee_deg_f[indices_min[3]]),
xytext=(0.5, 20),
color= 'r',
fontsize = 11,
weight = 'bold',
arrowprops=dict( facecolor="r", # Color de la flecha
edgecolor="r", # Contorno de la flecha
arrowstyle="->", # Estilo de flecha
linewidth=3 # Grosor de la flecha
))
plt.fill_between(t, knee_deg_f, where= (t>=0.4)& (t<=0.8), alpha=0.3, color="yellow") # Rellena el área bajo la curva
plt.text(0.62, 3, "Inicial double\nlimb support ", color='chocolate', fontsize=11, ha='center', weight='bold')
plt.fill_between(t, knee_deg_f, where= (t>=0.78)& (t<=1.01), alpha=0.3, color="purple") # Rellena el área bajo la curva
plt.text(0.9, 3, "Single\nlimb\nstance ", color='purple', fontsize=11, ha='center', weight='bold')
plt.fill_between(t, knee_deg_f, where= (t>=1.0)& (t<=1.08), alpha=0.3, color="blue") # Rellena el área bajo la curva
plt.text(1.04, 3, "Second\ndouble\nlimb\nsupport ", color='blue', fontsize=11, ha='center', weight='bold')
plt.fill_between(t, knee_deg_f, where= (t>=1.06)& (t<=1.16), alpha=0.3, color="red") # Rellena el área bajo la curva
plt.text(1.12, 3, "Inicial\nswimg", color='red', fontsize=11, ha='center', weight='bold')
plt.fill_between(t, knee_deg_f, where= (t>=1.15)& (t<=1.23), alpha=0.3, color="green") # Rellena el área bajo la curva
plt.text(1.2, 3, "Mid\nswimg", color='green', fontsize=11, ha='center', weight='bold')
plt.fill_between(t, knee_deg_f, where= (t>=1.23)& (t<=1.37), alpha=0.3, color="cyan") # Rellena el área bajo la curva
plt.text(1.27, 3, "Terminal\nswimg", color='darkcyan', fontsize=11, ha='center', weight='bold')
plt.xlim(0.3,1.45)
plt.title('Ángulo de la rodilla')
plt.xlabel('tiempo (s)')
plt.ylabel('Ángulo θ')
plt.grid()
plt.show()
2. Velocidad angular en las fases de la marcha¶
# print (velocidad_knee_f.iloc[5])
plt.figure(figsize=[20,8])
velocidad_knee_f = np.array(velocidad_knee_f, dtype=float)
t1 = np.array(t[1:] , dtype=float)
velocidad_knee_f = np.nan_to_num(velocidad_knee_f, nan=0) # Convierte NaN a 0
indice_min1 = np.argmin(velocidad_knee_f)
indice_max1 = np.argmax(velocidad_knee_f)
# Graficar
plt.plot(t1, velocidad_knee_f, 'black')
plt.axvline (x=t1[indices_min[0]], color='b', linestyle='--', linewidth=3)
plt.text(1.4, 30, "Fin de marcha", color='b', fontsize=12, ha='center', rotation= 90, weight = 'bold')
plt.axvline (x=t1[indices_min[3]], color='b', linestyle='--', linewidth=3)
plt.text(0.41, 30, "Inicio de marcha", color='b', fontsize=12, ha='center', rotation= 90, weight = 'bold')
# ANOTACIÓN DE MÁXIMA VELOCIDAD
plt.annotate(f"Velocidad máxima\n ({t1[indice_max1]:.2f}, {velocidad_knee_f[indice_max1]:.2f})",
xy=(t1[indice_max1], velocidad_knee_f[indice_max1]),
xytext=(0.8, 250),
color= 'r',
fontsize = 11,
weight = 'bold',
arrowprops=dict( facecolor="r", # Color de la flecha
edgecolor="r", # Contorno de la flecha
arrowstyle="->", # Estilo de flecha
linewidth=3 # Grosor de la flecha
))
# ANOTACIÓN DE MÍNIMA VELOCIDAD
plt.annotate(f"Velocidad 'mínima'\n ({t1[indice_min1]:.2f}, {velocidad_knee_f[indice_min1]:.2f})",
xy=(t1[indice_min1], velocidad_knee_f[indice_min1]),
xytext=(1.05, -250),
color= 'r',
fontsize = 11,
weight = 'bold',
arrowprops=dict( facecolor="r", # Color de la flecha
edgecolor="r", # Contorno de la flecha
arrowstyle="->", # Estilo de flecha
linewidth=3 # Grosor de la flecha
))
plt.fill_between(t1, velocidad_knee_f, where= (t1>=0.4)& (t1<=0.8), alpha=0.3, color="yellow") # Rellena el área bajo la curva
plt.text(0.52, 20, "Inicial double\nlimb support ", color='chocolate', fontsize=11, ha='center', weight='bold')
plt.fill_between(t1, velocidad_knee_f, where= (t1>=0.78)& (t1<=1.01), alpha=0.3, color="purple") # Rellena el área bajo la curva
plt.text(0.95, 20, "Single\nlimb\nstance ", color='purple', fontsize=11, ha='center', weight='bold')
plt.fill_between(t1, velocidad_knee_f, where= (t1>=1.0)& (t1<=1.08), alpha=0.3, color="blue") # Rellena el área bajo la curva
plt.text(1.04, 20, "Second\ndouble\nlimb\nsupport ", color='blue', fontsize=11, ha='center', weight='bold')
plt.fill_between(t1, velocidad_knee_f, where= (t1>=1.06)& (t1<=1.16), alpha=0.3, color="red") # Rellena el área bajo la curva
plt.text(1.12, 20, "Inicial\nswimg", color='red', fontsize=11, ha='center', weight='bold')
plt.fill_between(t1, velocidad_knee_f, where= (t1>=1.15)& (t1<=1.23), alpha=0.3, color="green") # Rellena el área bajo la curva
plt.text(1.2, -60, "Mid\nswimg", color='green', fontsize=11, ha='center', weight='bold')
plt.fill_between(t1, velocidad_knee_f, where= (t1>=1.23)& (t1<=1.4), alpha=0.3, color="cyan") # Rellena el área bajo la curva
plt.text(1.3, -60, "Terminal\nswimg", color='darkcyan', fontsize=11, ha='center', weight='bold')
plt.xlim(0.3,1.45)
plt.title('Velocidad angular de la rodilla')
plt.xlabel('tiempo (s)')
plt.ylabel('Velocidad (°/s)')
plt.grid()
plt.show()
3. Aceleración angular en las fases de la marcha¶
# print (aceleracion_knee_f.iloc[5])
plt.figure(figsize=[20,8])
aceleracion_knee_f = np.array(aceleracion_knee_f, dtype=float)
t1 = np.array(t[2:] , dtype=float)
aceleracion_knee_f = np.nan_to_num(aceleracion_knee_f, nan=0) # Convierte NaN a 0
indices_min2 = np.abs(aceleracion_knee_f-(min(aceleracion_knee_f))).argsort()[:4]
indice_max2 = np.abs(aceleracion_knee_f-(max(aceleracion_knee_f))).argsort()[:4]
# Graficar
plt.plot(t1, aceleracion_knee_f, 'black')
plt.axvline (x=t1[indices_min[0]], color='b', linestyle='--', linewidth=3)
plt.text(1.42, 30, "Fin de marcha", color='b', fontsize=12, ha='center', rotation= 90, weight = 'bold')
plt.axvline (x=t1[indices_min[3]], color='b', linestyle='--', linewidth=3)
plt.text(0.4, 30, "Inicio de marcha", color='b', fontsize=12, ha='center', rotation= 90, weight = 'bold')
# ANOTACIÓN DE MÁXIMA VELOCIDAD
plt.annotate(f"Aceleración máxima\n(del ciclo de marcha)\n ({t1[indice_max2[3]]:.2f}, {aceleracion_knee_f[indice_max2[3]]:.2f})",
xy=(t1[indice_max2[3]], aceleracion_knee_f[indice_max2[3]]),
xytext=(0.6, 2000),
color= 'r',
fontsize = 11,
weight = 'bold',
arrowprops=dict( facecolor="r", # Color de la flecha
edgecolor="r", # Contorno de la flecha
arrowstyle="->", # Estilo de flecha
linewidth=3 # Grosor de la flecha
))
# ANOTACIÓN DE MÍNIMA VELOCIDAD
plt.annotate(f"Aceleración mínima\n(del ciclo de marcha)\n ({t1[indices_min2[0]]:.2f}, {aceleracion_knee_f[indices_min2[0]]:.2f})",
xy=(t1[indices_min2[0]], aceleracion_knee_f[indices_min2[0]]),
xytext=(1, -2000),
color= 'r',
fontsize = 11,
weight = 'bold',
arrowprops=dict( facecolor="r", # Color de la flecha
edgecolor="r", # Contorno de la flecha
arrowstyle="->", # Estilo de flecha
linewidth=3 # Grosor de la flecha
))
plt.fill_between(t1, aceleracion_knee_f, where= (t1>=0.41)& (t1<=0.8), alpha=0.3, color="yellow") # Rellena el área bajo la curva
plt.text(0.48, 1000, "Inicial double\nlimb support ", color='chocolate', fontsize=11, ha='center', weight='bold')
plt.fill_between(t1, aceleracion_knee_f, where= (t1>=0.78)& (t1<=1.01), alpha=0.3, color="purple") # Rellena el área bajo la curva
plt.text(0.95, 500, "Single\nlimb\nstance ", color='purple', fontsize=11, ha='center', weight='bold')
plt.fill_between(t1, aceleracion_knee_f, where= (t1>=1.0)& (t1<=1.08), alpha=0.3, color="blue") # Rellena el área bajo la curva
plt.text(1.04, 20, "Second\ndouble\nlimb\nsupport ", color='blue', fontsize=11, ha='center', weight='bold')
plt.fill_between(t1, aceleracion_knee_f, where= (t1>=1.06)& (t1<=1.16), alpha=0.3, color="red") # Rellena el área bajo la curva
plt.text(1.14, -500, "Inicial\nswimg", color='red', fontsize=11, ha='center', weight='bold')
plt.fill_between(t1, aceleracion_knee_f, where= (t1>=1.15)& (t1<=1.23), alpha=0.3, color="green") # Rellena el área bajo la curva
plt.text(1.2, -1000, "Mid\nswimg", color='green', fontsize=11, ha='center', weight='bold')
plt.fill_between(t1, aceleracion_knee_f, where= (t1>=1.23)& (t1<=1.41), alpha=0.3, color="cyan") # Rellena el área bajo la curva
plt.text(1.3, -1000, "Terminal\nswimg", color='darkcyan', fontsize=11, ha='center', weight='bold')
plt.xlim(0.3,1.45)
plt.title('Aceleración angular de la rodilla')
plt.xlabel('tiempo (s)')
plt.ylabel('Aceleración (°/s^2)')
plt.grid()
plt.show()
4. Tiempo de duración de la marcha¶
# Conociendo los valores del tiempo donde inicia y termina la marcha
# kos cuales corresponden a los minimos indices y hallados para los ángulos
tiempo_paso = t[indices_min[0]]-t[indices_min[3]]
print(f"La duración de cada paso es de: {tiempo_paso}")
La duración de cada paso es de: 0.987
IV. CONCLUSIÓN¶
El análisis del ciclo de marcha mostró cómo varían el ángulo, la velocidad y la aceleración de la rodilla en cada fase. Durante Initial Double Limb Support, la rodilla alcanza su máxima extensión, lo que permite estabilidad al transferir peso entre piernas. La aceleración positiva indica un impulso inicial. En Single Limb Stance, el ángulo se mantiene estable, reflejando un periodo de equilibrio sobre una sola pierna. Second Double Limb Support marca el inicio de la flexión de la rodilla, preparando el despegue del pie.
En Initial Swing, la rodilla alcanza su flexión máxima para permitir el avance del pie sin contacto con el suelo. Aquí, la velocidad angular es mayor, facilitando el movimiento. En Mid Swing, la aceleración negativa indica un freno progresivo para controlar la extensión. Finalmente, en Terminal Swing, la rodilla se extiende nuevamente, preparando el contacto con el suelo de manera controlada.
Este análisis resalta la compleja coordinación biomecánica necesaria para una marcha eficiente, lo que resulta clave en la rehabilitación y desarrollo de tecnologías asistivas.
V. REFERENCIAS¶
[1] S. Collado-Vazquez,F. Gómez, A. Vadillo, L. Rodríguez, Análisis de la marcha. Factores Moduladores. Biociencias, Vol. 1, No. 1, 2003.
[2] A. Martín-Nogueras, J. L. Calvo-Arenillas, J. Orejuela-Rodríguez, F. J. Barbero-Iglesias, C. Sánchez-Sánchez. Fases de la marcha humana, Revista Iberoamericana de Fisioterapia y Kinesiología, Vol., No. 1, pp 44-49, 1998.
[3] S. Zulkifli and Wei-Ping Loh. A state-of-the-art review of foot pressure. Foot and Ankle Surgery, Vol 26, Issue 1, 25-32. 2020.